package pl.polidea.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

/**
 * Horizontal pages view.
 * 
 * @author karooolek
 */
public class HorizontalPagesView extends FrameLayout {

    /**
     * Horizontal pages view listener.
     * 
     * @author karooolek
     * 
     */
    public interface HorizontalPagesViewListener {
        void onPageSelect(View page, int position);
    }

    // pages adapter
    private AbstractPagesAdapter mAdapter;
    private int mFirstPage, mCurrentPage, mLastPage;

    // listener
    private HorizontalPagesViewListener mListener;

    // touch variables
    private float mSwipeSpeed = 10.0f;
    private float mLastX, mLastY;
    private float mStartX, mStartY;
    private boolean mTouched;
    private long mLastTouchTime;

    // scrolling variables
    private boolean mScrollingEnabled = true;
    private float mBouncing = 0.4f;
    private float mScrollDamping = 0.2f;
    private int mScrollDirection; // NOPMD by karooolek on 29.06.11 11:49
    private float scrollX, scrollY;
    private float velX, velY;
    private float scrollXto, scrollYto;
    private float scrollOffset;

    // internal layout for pages
    private final FrameLayout internalLayout;

    public HorizontalPagesView(final Context context) {
        super(context);

        // create internal layout
        addView(internalLayout = new FrameLayout(context), new FrameLayout.LayoutParams(9999999, 99999, Gravity.LEFT));
        internalLayout.setWillNotDraw(true);
        internalLayout.setDrawingCacheEnabled(false);
        internalLayout.setAnimationCacheEnabled(false);
    }

    public HorizontalPagesViewListener getListener() {
        return mListener;
    }

    public void setListener(final HorizontalPagesViewListener listener) {
        mListener = listener;
    }

    public boolean isScrollingEnabled() {
        return mScrollingEnabled;
    }

    public void setScrollingEnabled(final boolean scrollingEnabled) {
        mScrollingEnabled = scrollingEnabled;
    }

    public float getPageSwitchSwipeSpeed() {
        return mSwipeSpeed;
    }

    public void setPageSwitchSwipeSpeed(final float swipeSpeed) {
        if (swipeSpeed <= 0.0f) {
            return;
        }
        mSwipeSpeed = swipeSpeed;
    }

    public float getBouncingFactor() {
        return mBouncing;
    }

    public void setBouncingFactor(final float bouncing) {
        if (bouncing <= 0.0f || bouncing > 1.0f) {
            return;
        }
        mBouncing = bouncing;
    }

    public float getScrollDampingFactor() {
        return mScrollDamping;
    }

    public void setScrollDampingFactor(final float scrollDamping) {
        if (scrollDamping <= 0.0f || scrollDamping > 1.0f) {
            return;
        }
        mScrollDamping = scrollDamping;
    }

    public AbstractPagesAdapter getPageAdapter() {
        return mAdapter;
    }

    public void setPageAdapter(final AbstractPagesAdapter adapter) {
        setPageAdapter(adapter, adapter.getMinimumPosition());
    }

    public void setPageAdapter(final AbstractPagesAdapter adapter, final int position) {
        mAdapter = adapter;
        scrollToPage(position);
    }

    private int getAdjustedPageWidth(final int i) {
        final int w = mAdapter.getPageWidth(i);
        return w < 0 ? getWidth() : w;
    }

    private int getAdjustedPageHeight(final int i) {
        final int h = mAdapter.getPageHeight(i);
        return h < 0 ? getHeight() : h;
    }

    private void selectPage(final int i) {
        mCurrentPage = i;

        // call listener
        if (mListener != null) {
            mListener.onPageSelect(getSelectedPage(), mCurrentPage);
        }
    }

    private void addPageOnLeft() {
        --mFirstPage;

        final int w = getAdjustedPageWidth(mFirstPage);
        final int h = getAdjustedPageHeight(mFirstPage);

        scrollOffset -= w;

        final LayoutParams lparams = new FrameLayout.LayoutParams(w, h, Gravity.LEFT);
        lparams.leftMargin = (int) scrollOffset;
        lparams.topMargin = 0;
        final View v = mAdapter.getView(mFirstPage);
        internalLayout.addView(v, 0, lparams);
        v.setMinimumWidth(w);
        v.setMinimumHeight(h);
        v.measure(0, 0);
    }

    private void removePageFromLeft() {
        scrollOffset += getAdjustedPageWidth(mFirstPage);

        if (internalLayout.getChildCount() > 0) {
            internalLayout.removeViewAt(0);
        }

        ++mFirstPage;
    }

    private void addPageOnRight() {
        final int w = getAdjustedPageWidth(mLastPage);
        final int h = getAdjustedPageHeight(mLastPage);

        float r = scrollOffset;
        for (int i = mFirstPage; i <= mLastPage; ++i) {
            r += getAdjustedPageWidth(i);
        }

        final LayoutParams lparams = new FrameLayout.LayoutParams(w, h, Gravity.LEFT);
        lparams.leftMargin = (int) r;
        lparams.topMargin = 0;
        final View v = mAdapter.getView(mLastPage);
        internalLayout.addView(v, -1, lparams);
        v.setMinimumWidth(w);
        v.setMinimumHeight(h);
        v.measure(0, 0);

        ++mLastPage;
    }

    private void removePageFromRight() {
        --mLastPage;

        if (internalLayout.getChildCount() > 0) {
            internalLayout.removeViewAt(internalLayout.getChildCount() - 1);
        }
    }

    private void preparePages() {
        // loading margin
        final float margin = -0.5f;// 0.500f * getWidth();

        // load previous pages
        while (scrollX - margin < scrollOffset && mFirstPage > mAdapter.getMinimumPosition()) {
            addPageOnLeft();
        }

        float r = scrollOffset;
        for (int i = mFirstPage; i < mLastPage; ++i) {
            r += getAdjustedPageWidth(i);
        }

        // load next pages
        while (r < scrollX + getWidth() + margin && mLastPage <= mAdapter.getMaximumPosition()) {
            addPageOnRight();
            r += getAdjustedPageWidth(mLastPage - 1);
        }

        // unload previous pages
        while (mFirstPage < mAdapter.getMaximumPosition()
                && scrollOffset + getAdjustedPageWidth(mFirstPage) < scrollX - margin) {
            removePageFromLeft();
        }

        // unload next pages
        while (mLastPage > mAdapter.getMinimumPosition()
                && scrollX + getWidth() + margin < r - getAdjustedPageWidth(mLastPage - 1)) {
            removePageFromRight();
            r -= getAdjustedPageWidth(mLastPage - 1);
        }
    }

    public void dispatchTouchEventToSelectedPage(final MotionEvent ev) {
        final View v = getSelectedPage();
        if (v != null) {
            float currLeft = scrollOffset;
            for (int i = mFirstPage; i < mCurrentPage; ++i) {
                currLeft += getAdjustedPageWidth(i);
            }
            ev.setLocation(ev.getX() + scrollX - currLeft, ev.getY() + scrollY);
            v.dispatchTouchEvent(ev);
        }
    }

    @Override
    public boolean dispatchTouchEvent(final MotionEvent ev) {

        // force redraw
        getRootView().invalidate();
        invalidate();

        // no adapter
        if (mAdapter == null) {
            return false;
        }

        // scrolling is disabled
        if (!mScrollingEnabled) {
            dispatchTouchEventToSelectedPage(ev);
            return true;
        }

        // no multi-touch
        if (ev.getPointerCount() > 1) {
            dispatchTouchEventToSelectedPage(ev);
            return true;
        }

        processSingleTouchEvent(ev);

        return true;
    }

    private void processSingleTouchEvent(final MotionEvent ev) {
        mLastTouchTime = System.currentTimeMillis();
        final float x = ev.getX();
        final float y = ev.getY();
        float lx = x - mStartX;
        float ly = y - mStartY;
        final float l = (float) Math.hypot(lx, ly);
        float dx = x - mLastX;
        float dy = y - mLastY;
        mLastX = x;
        mLastY = y;

        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouched = true;
            mStartX = x;
            mStartY = y;
            mLastX = x;
            mLastY = y;
            dx = 0;
            dy = 0;
            lx = 0;
            ly = 0;
            mScrollDirection = -1;

            dispatchTouchEventToSelectedPage(ev);
            break;

        case MotionEvent.ACTION_MOVE:
            // determine scrolling direction
            if (mScrollDirection == -1 && l > 30.0f) {
                if (Math.abs(dx) > Math.abs(dy)) { // its horizontal scrolling
                    mScrollDirection = 1;

                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    dispatchTouchEventToSelectedPage(ev);
                } else { // its vertical scrolling
                    mTouched = false;
                    mScrollDirection = 2;
                }
            }

            if (mScrollDirection == 1) { // horizontal scrolling
                velX = Math.abs(dx) > 0.1f ? -dx : velX;
                velY = 0.0f;// -dy;
                smoothScrollBy(velX, velY);
            } else { // undetermined or vertical scrolling
                dispatchTouchEventToSelectedPage(ev);
            }
            break;

        case MotionEvent.ACTION_OUTSIDE:
        case MotionEvent.ACTION_UP:
            mTouched = false;

            if (mScrollDirection == 1) { // horizontal scrolling
                float currLeft = scrollOffset;
                for (int i = mFirstPage; i < mCurrentPage; ++i) {
                    currLeft += getAdjustedPageWidth(i);
                }
                final float currWidth = getAdjustedPageWidth(mCurrentPage);
                final float currRight = currLeft + currWidth;
                if ((velX >= mSwipeSpeed && (currWidth <= getWidth() || scrollX > currRight - getWidth()) || currRight
                        - scrollX < 0.5f * getWidth())
                        && mCurrentPage < mAdapter.getMaximumPosition()) {
                    velX = 0.0f;
                    velY = 0.0f;
                    selectPage(++mCurrentPage);
                } else if ((velX <= -mSwipeSpeed && (currWidth <= getWidth() || scrollX < currLeft) || currLeft
                        - scrollX > 0.5f * getWidth())
                        && mCurrentPage > mAdapter.getMinimumPosition()) {
                    velX = 0.0f;
                    velY = 0.0f;
                    selectPage(--mCurrentPage);
                }
            } else { // undetermined or vertical
                dispatchTouchEventToSelectedPage(ev);
            }
            break;

        case MotionEvent.ACTION_CANCEL:
            mTouched = false;

            dispatchTouchEventToSelectedPage(ev);
            break;

        default:
        }
    }

    private float clamp(final float min, final float value, final float max) {
        return Math.max(min, Math.min(value, max));
    }

    private float lerp(final float a, final float b, final float k) {
        return a + (b - a) * k;
    }

    private float bias(final float a, final float b, final float k) {
        return Math.abs(b - a) >= k ? a + k * Math.signum(b - a) : b;
    }

    public void scrollTo(final float x, final float y) {
        scrollX = x;
        scrollY = y;
        internalLayout.scrollTo((int) scrollX, (int) scrollY);
    }

    public void scrollBy(final float dx, final float dy) {
        scrollX += dx;
        scrollY += dy;
        internalLayout.scrollTo((int) scrollX, (int) scrollY);
    }

    public void smoothScrollTo(final float x, final float y) {
        scrollXto = x;
        scrollYto = y;
    }

    public void smoothScrollBy(final float dx, final float dy) {
        scrollXto += dx;
        scrollYto += dy;
    }

    private void smoothScrolling() {
        scrollX = lerp(bias(scrollX, scrollXto, 1.0f), scrollXto, mBouncing);
        scrollY = lerp(bias(scrollY, scrollYto, 1.0f), scrollYto, mBouncing);
    }

    private void bounceScrolling() {
        float minX = scrollOffset;
        if (mFirstPage < mCurrentPage) {
            for (int i = mFirstPage; i < mCurrentPage; ++i) {
                minX += getAdjustedPageWidth(i);
            }
        } else if (mCurrentPage < mFirstPage) {
            for (int i = mFirstPage; i > mCurrentPage; --i) {
                minX -= getAdjustedPageWidth(i - 1);
            }
        }
        float maxX = minX + getAdjustedPageWidth(mCurrentPage) - getWidth();
        if (maxX < minX) {
            minX = 0.5f * (minX + maxX);
            maxX = minX;
        }

        float minY = 0.0f;
        float maxY = 0.0f;
        if (maxY < minY) {
            minY = 0.5f * (minY + maxY);
            maxY = minY;
        }

        smoothScrollTo(clamp(minX, scrollX, maxX), clamp(minY, scrollY, maxY));
    }

    private void dampScrolling() {
        velX = lerp(bias(velX, 0.0f, 1.0f), 0.0f, mScrollDamping);
        velY = lerp(bias(velY, 0.0f, 1.0f), 0.0f, mScrollDamping);
        smoothScrollBy(velX, velY);
    }

    public void clampScrolling() {
        bounceScrolling();
        scrollTo(scrollXto, scrollYto);
    }

    public View getSelectedPage() {
        return internalLayout.getChildAt(mCurrentPage - mFirstPage);
    }

    public int getSelectedPagePosition() {
        return mCurrentPage;
    }

    public void scrollToPage(final int position) {
        // bad outside range
        if (position < mAdapter.getMinimumPosition() || position > mAdapter.getMaximumPosition()) {
            return;
        }

        // select page
        internalLayout.removeAllViews();
        selectPage(position);
        mFirstPage = mLastPage = position;
        clampScrolling();
    }

    public void smoothScrollToPage(final int position) {
        // we are on selected page
        if (position == mCurrentPage) {
            return;
        }

        // bad outside range
        if (position < mAdapter.getMinimumPosition() || position > mAdapter.getMaximumPosition()) {
            return;
        }

        // select page
        selectPage(position);
    }

    @Override
    protected void dispatchDraw(final Canvas canvas) {
        // get timing
        final long t = System.currentTimeMillis();

        // touch timed out
        if (mTouched && t - mLastTouchTime > 2000) {
            mTouched = false;
        }

        // draw background
        final Drawable bg = getBackground();
        if (bg != null) {
            bg.draw(canvas);
        }

        // no pages
        if (mAdapter == null || mAdapter.getCount() == 0) {
            return;
        }

        // load first page
        if (getChildCount() == 0) {
            addPageOnRight();
        }

        // animate if not touched
        if (!mTouched) {
            bounceScrolling();
            dampScrolling();
        }

        // apply smooth scrolling
        smoothScrolling();

        // prepare pages
        preparePages();

        // draw pages
        float l = scrollOffset;
        for (int i = 0; i != internalLayout.getChildCount(); ++i) {
            // get view and dimensions
            final View v = internalLayout.getChildAt(i);

            canvas.save();
            canvas.translate(l - scrollX, -scrollY);
            canvas.clipRect(0, 0, getAdjustedPageWidth(mFirstPage + i), getAdjustedPageHeight(mFirstPage + i));
            canvas.translate(-v.getScrollX(), -v.getScrollY());
            v.draw(canvas);
            canvas.restore();

            l += getAdjustedPageWidth(mFirstPage + i);
        }

        // if (Math.abs(velX - 0.0f) < 0.0000001f && Math.abs(velY - 0.0f) <
        // 0.0000001f
        // && Math.abs(scrollX - scrollXto) < 0.0000001f && Math.abs(scrollY -
        // scrollYto) < 0.0000001f) {
        // }

        // redraw frame
        getRootView().invalidate();
        invalidate();
    }

    @Override
    protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        internalLayout.removeAllViews();
        mFirstPage = mLastPage = mCurrentPage;
    }
}
